﻿//*********************************************************
//
//    Copyright (c) Microsoft. All rights reserved.
//    This code is licensed under the Microsoft Public License.
//    THIS CODE IS PROVIDED *AS IS* WITHOUT WARRANTY OF
//    ANY KIND, EITHER EXPRESS OR IMPLIED, INCLUDING ANY
//    IMPLIED WARRANTIES OF FITNESS FOR A PARTICULAR
//    PURPOSE, MERCHANTABILITY, OR NON-INFRINGEMENT.
//
//*********************************************************

namespace AstoriaOverAstoria
{
    using System;
    using System.Collections.Generic;
    using System.Data.Services.Client;
    using System.Data.Services.Internal;
    using System.Data.Services.Providers;
    using System.Diagnostics;
    using System.Linq;
    using System.Linq.Expressions;
    using System.Reflection;
    using System.Text;

    /// <summary><see cref="IQueryResultsProcessor"/> which turns entity instances
    /// into new ProjectedWrapper instances which can be consumed by the server.</summary>
    /// <remarks>This class servers several purposes:
    ///    - Has public methods to create the description according to which the results should be transformed.
    ///    - Stores the definition of the transformation of the results.
    ///    - Before processing it compiles the transformation into a function which is then used (for performance reasons)
    ///    - It implements the <see cref="IQueryResultsProcessor"/> and is called to process the results of the query.
    /// </remarks>
    internal class ProjectedWrapperQueryResultsProcessor : IQueryResultsProcessor
    {
        /// <summary>List of projected properties.</summary>
        private List<ProjectedProperty> projectedProperties;

        /// <summary>The ProjectedWrapper type which should be returned.</summary>
        private Type projectedWrapperType;

        /// <summary>Parameter expression which represents the source result to be processed.</summary>
        private ParameterExpression sourceElementParameter;

        /// <summary>List of resource types which can be projected.</summary>
        /// <remarks>This list is ordered such that the most derived types go first and so the search through this list should be done
        /// using IsAssignableFrom in the order of the list.</remarks>
        private List<ResourceType> resourceTypes;

        /// <summary>Parameter expression which represents the <see cref="AstoriaOverAstoriaContext"/> on which the query executes.</summary>
        private ParameterExpression contextParameter;

        /// <summary>Cached compiled function to process the results.</summary>
        private Func<object, AstoriaOverAstoriaContext, object> processSingleResult;

        /// <summary>Creates new query results processor.</summary>
        /// <param name="sourceElementType">The type of the source result. (not a projected wrapper type)</param>
        /// <param name="projectedWrapperType">The projected wrapper type which should be returned.</param>
        /// <param name="resourceTypes">List of resource types for this projection.
        /// This list should be ordered such that more derived type goes before less derived type.</param>
        public ProjectedWrapperQueryResultsProcessor(Type sourceElementType, Type projectedWrapperType, IEnumerable<ResourceType> resourceTypes)
        {
            Debug.Assert(sourceElementType != null, "sourceElementType != null");
            Debug.Assert(projectedWrapperType != null, "projectedWrapperType != null");
            Debug.Assert(resourceTypes != null, "projectedTypes != null");
            Debug.Assert(!TypeSystem.IsProjectedWrapperType(sourceElementType), "The source element type is a ProjectedWrapper, that should not happen.");
            Debug.Assert(TypeSystem.IsProjectedWrapperType(projectedWrapperType), "The projected wrapper type is not ProjectedWrapper.");
            Debug.Assert(resourceTypes.Count() > 0, "Some projected types must be specified.");
            Debug.Assert(resourceTypes.All(t => sourceElementType.IsAssignableFrom(t.InstanceType)), "Some projected type does not inherit from the source element type.");

            this.projectedProperties = new List<ProjectedProperty>();
            this.projectedWrapperType = projectedWrapperType;
            this.sourceElementParameter = Expression.Parameter(sourceElementType, "element");
            this.contextParameter = Expression.Parameter(typeof(AstoriaOverAstoriaContext), "context");
            this.resourceTypes = new List<ResourceType>(resourceTypes);
        }

        /// <summary>Adds projected property.</summary>
        /// <param name="propertyName">The name of the projected property (on the source entity).</param>
        /// <param name="projectedIndex">The index of the projected property (on the returned ProjectedWrapper).</param>
        public void AddProjectedProperty(string propertyName, int projectedIndex)
        {
            ProjectedProperty property = new ProjectedProperty
            { 
                PropertyName = propertyName, 
                ProjectedIndex = projectedIndex,
                PropertyAccess = Expression.Property(this.sourceElementParameter, propertyName),
            };

            this.projectedProperties.Add(property);
        }

        #region IQueryResultsProcessor implementation

        /// <summary>Method which processes a single result of a query before it is handed to the server.</summary>
        /// <param name="result">The result produced by the query (and any result processors) so far.</param>
        /// <param name="context">The context which is executing the query.</param>
        /// <returns>The result of the query after the transformation.</returns>
        object IQueryResultsProcessor.ProcessSingleResult(object result, AstoriaOverAstoriaContext context)
        {
            this.InitializeResultProcessing();
            return this.processSingleResult(result, context);
        }

        /// <summary>Method which processes the results of a query before they are handed to the server.</summary>
        /// <param name="results">The results of the query so far.</param>
        /// <param name="context">The context which is executing the query.</param>
        /// <returns>The results of the query after the transformation.</returns>
        System.Collections.IEnumerable IQueryResultsProcessor.ProcessResults(System.Collections.IEnumerable results, AstoriaOverAstoriaContext context)
        {
            this.InitializeResultProcessing();
            foreach (object result in results)
            {
                yield return this.processSingleResult(result, context);
            }
        }

        #endregion

        /// <summary>Helper method which is called from the compiled function to determine the type name of the specified resource.</summary>
        /// <param name="resourceTypes">List of projected resource types.</param>
        /// <param name="resource">The resource instance to get the type for.</param>
        /// <returns>The full name of the resource type of the <paramref name="resource"/>.</returns>
        [System.Diagnostics.CodeAnalysis.SuppressMessage("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode",
            Justification = "Called dynamically through reflection.")]
        private static string GetResourceTypeName(List<ResourceType> resourceTypes, object resource)
        {
            Type clientType = resource.GetType();

            foreach (var resourceType in resourceTypes)
            {
                if (resourceType.InstanceType.IsAssignableFrom(clientType))
                {
                    return resourceType.FullName;
                }
            }

            throw new NotSupportedException("Can't resolve resource type for client CLR type '" + clientType.ToString() + "'.");
        }

        /// <summary>Initializes the function used to process the results.</summary>
        private void InitializeResultProcessing()
        {
            if (this.processSingleResult == null)
            {
                // It's much easier to construct the desired behavior using LINQ to Objects, then compile the lambda and run it
                //    then doing this using reflection. It's also much faster that way.
                // For more compilcated stuff we can call helper methods anyway.

                // Sort the properties by their projected index
                this.projectedProperties.Sort((x, y) => x.ProjectedIndex == y.ProjectedIndex ? 0 : (x.ProjectedIndex < y.ProjectedIndex ? -1 : 1));

                LambdaExpression lambda = Expression.Lambda(
                    this.CreateNewProjectedWrapperExpression(0),
                    this.sourceElementParameter,
                    this.contextParameter);
                ParameterExpression untypedSourceElementParameter = Expression.Parameter(typeof(object), "untypedElement");
                Expression invokeTypedLambda = Expression.Convert(
                    Expression.Invoke(
                        lambda,
                        Expression.Convert(untypedSourceElementParameter, this.sourceElementParameter.Type),
                        this.contextParameter), 
                    typeof(object));
                lambda = Expression.Lambda(
                    Expression.Condition(
                        Expression.Equal(untypedSourceElementParameter, Expression.Constant(null, typeof(object))),
                        Expression.Constant(null, typeof(object)),
                        invokeTypedLambda),
                    untypedSourceElementParameter,
                    this.contextParameter);
                this.processSingleResult = (Func<object, AstoriaOverAstoriaContext, object>)lambda.Compile();
            }
        }

        /// <summary>Creates expression tree which converts the results.</summary>
        /// <param name="startPropertyIndex">The index of the projected property to start from - used when ProjectedWrapperMany is used when dealing with the Next property.</param>
        /// <returns>Expression which evaluates to the ProjectedWrapper to be returned to the server.</returns>
        /// <remarks>This expression is compiled into a function used to process the results.</remarks>
        private Expression CreateNewProjectedWrapperExpression(int startPropertyIndex)
        {
            Debug.Assert(
                startPropertyIndex == 0 || this.projectedWrapperType == typeof(ProjectedWrapperMany),
                "Only ProjectedWrapperMany supports nesting.");

            int projectedPropertyCount = this.projectedProperties.Count - startPropertyIndex;

            // Bind all properties plus RersourceTypeName and PropertyNameList
            int bindingsCount = projectedPropertyCount + 2;
            if (this.projectedWrapperType == typeof(ProjectedWrapperMany))
            {
                projectedPropertyCount = projectedPropertyCount > 8 ? 8 : projectedPropertyCount;

                // We will need to bind the Next property as well
                bindingsCount = projectedPropertyCount + 3;
            }

            MemberBinding[] bindings = new MemberBinding[bindingsCount];

            // ResourceTypeName - we have to "compute" this by asking the context, so use helper method instead
            Expression callGetResourceTypeName = Expression.Call(
                this.GetType().GetMethod("GetResourceTypeName", BindingFlags.NonPublic | BindingFlags.Static),
                Expression.Constant(this.resourceTypes, typeof(List<ResourceType>)),
                this.sourceElementParameter);
            bindings[0] = Expression.Bind(
                this.projectedWrapperType.GetProperty("ResourceTypeName"),
                callGetResourceTypeName);

            StringBuilder propertyNameList = new StringBuilder();
            if (startPropertyIndex == 0)
            {
                int totalProjectedProperties = this.projectedProperties.Count;
                for (int index = 0; index < totalProjectedProperties; index++)
                {
                    if (propertyNameList.Length > 0)
                    {
                        propertyNameList.Append(',');
                    }

                    propertyNameList.Append(this.projectedProperties[index].PropertyName);
                }
            }

            // projected properties
            for (int index = 0; index < projectedPropertyCount; index++)
            {
                ProjectedProperty property = this.projectedProperties[index + startPropertyIndex];

                bindings[2 + index] = Expression.Bind(
                    this.projectedWrapperType.GetProperty(TypeSystem.GetProjectedPropertyName(index)),
                    Expression.Convert(property.PropertyAccess, typeof(object)));
            }

            // PropertyNameList
            bindings[1] = Expression.Bind(
                this.projectedWrapperType.GetProperty("PropertyNameList"),
                Expression.Constant(propertyNameList.ToString()));

            // Next (if needed)
            if (this.projectedWrapperType == typeof(ProjectedWrapperMany))
            {
                Expression expr;
                if (this.projectedProperties.Count - startPropertyIndex > 8)
                {
                    // Recursively new up the next wrapper
                    expr = this.CreateNewProjectedWrapperExpression(startPropertyIndex + 8);
                }
                else
                {
                    // No more wrappers needed - end it with null
                    expr = Expression.Constant(null, typeof(ProjectedWrapperMany));
                }

                bindings[bindingsCount - 1] = Expression.Bind(
                    typeof(ProjectedWrapperMany).GetProperty("Next"),
                    expr);
            }

            return Expression.MemberInit(
                Expression.New(this.projectedWrapperType),
                bindings);
        }

        /// <summary>Helper class to store information about projected property.</summary>
        private class ProjectedProperty
        {
            /// <summary>The name of the projected property (on the source resouce).</summary>
            public string PropertyName { get; set; }

            /// <summary>The index of the projected property (on the target ProjectedWrapper).</summary>
            public int ProjectedIndex { get; set; }

            /// <summary>Expression to access the property on the source resource.</summary>
            public Expression PropertyAccess { get; set; }
        }
    }
}